package main

import (
	"context"
	"database/sql"
	"fmt"
	_ "go.manyji.com/im3/app/syncMembers"
	"go.manyji.com/lib/log"
	"go.manyji.com/lib/mysql"
	server "go.manyji.com/uappServer"
	"reflect"
	"strings"
	"time"
)

var (
	db *DB
)

func init() {
	server.AfterDbOpen(func() {

		db = &DB{
			server.DB(),
		}
	})
}

func WaitInit() {
	for db == nil {
		time.Sleep(time.Millisecond * 10)
	}
}

type DB struct {
	*mysql.DB
}

func (d *DB) Create(ctx context.Context, tableName string, items map[string]interface{}) (sql.Result, error) {

	var (
		fields []string
		values []interface{}
		should []string
	)

	for k, v := range items {

		fields = append(fields, fmt.Sprintf("`%v`", k))
		values = append(values, v)
		should = append(should, "?")
	}

	insertSQL := fmt.Sprintf("INSERT INTO %s(%v) VALUES(%v)",
		tableName,
		strings.Join(fields, ","),
		strings.Join(should, ","),
	)

	log.Debug(fmt.Sprintf("sql:%v,values:%v", insertSQL, values))

	return d.Exec(insertSQL, values...)
}

func (d *DB) SoftDelete(ctx context.Context, tableName string, id uint64) error {

	var (
		now        = time.Now().Unix()
		updateKeys = map[string]interface{}{
			"deleted":    1,
			"updated_at": now,
		}
		queryKeys = map[string]interface{}{
			"id": id,
		}
	)
	return d.update(ctx, tableName, updateKeys, queryKeys)
}

func (d *DB) Update(ctx context.Context, tableName string, updateKeys map[string]interface{}, queryKeys map[string]interface{}) error {

	return d.update(ctx, tableName, updateKeys, queryKeys)
}

func (d *DB) ExecRaw(ctx context.Context, rawSql string) (sql.Result, error) {

	log.Info(fmt.Sprintf("exec sql:%+v", rawSql))

	var (
		err    error
		result sql.Result
	)
	if tx, ok := ctx.Value("transaction").(*sql.Tx); ok {
		result, err = tx.ExecContext(ctx, rawSql)
	} else {
		result, err = db.ExecContext(ctx, rawSql)
	}
	return result, err
}

func (d *DB) update(ctx context.Context, tableName string, updateKeys map[string]interface{}, queryKeys map[string]interface{}) error {

	var (
		values  []interface{}
		updates []string
		queries = []string{
			"deleted = 0",
		}
	)

	for k, v := range updateKeys {

		if v == nil {
			continue
		}
		vv := v
		if reflect.TypeOf(v).Kind() == reflect.String {
			vv = fmt.Sprintf("'%v'", v)
		}
		updates = append(updates, fmt.Sprintf("`%v` = %v", k, vv))
	}

	for k, v := range queryKeys {

		vv := v
		if reflect.TypeOf(v).Kind() == reflect.String {
			vv = fmt.Sprintf("'%v'", v)
		}
		queries = append(queries, fmt.Sprintf("%v = %v", k, vv))
	}

	s := fmt.Sprintf("UPDATE %s SET %v WHERE %v",
		tableName,
		strings.Join(updates, ","),
		strings.Join(queries, " AND "),
	)

	log.Info(fmt.Sprintf("sql:%v values:%v", s, values))

	var err error
	if tx, ok := ctx.Value("transaction").(*sql.Tx); ok {
		_, err = tx.Exec(s, values...)
	} else {
		_, err = db.Exec(s, values...)
	}
	return err
}

func (d *DB) Query(ctx context.Context, e Entity, db *mysql.DB, reqArgs map[string]interface{}, p PaginationParam, repKeys []string) (map[string]interface{}, error) {

	var (
		//opMap = map[string]string{
		//	"eq": "<>",
		//	"=":  "=",
		//	"IN": "IN",
		//}
		tableName  = e.TableName()
		conditions = []string{
			fmt.Sprintf("%v.deleted = 0", e.TableName()),
		}
		repKeyMap       = map[string]struct{}{}
		allKeys         bool
		emptyPagination = PaginationParam{}
		orderBy         = fmt.Sprintf("ORDER BY %v.create_at DESC", e.TableName())

		groupBy string
		ret     = map[string]interface{}{}
	)

	if _, ok := reqArgs["start"]; ok {
		if _, ok := reqArgs["end"]; ok {
			start, end := reqArgs["start"].(uint64), reqArgs["end"].(uint64)
			s := fmt.Sprintf("(%v.create_at >= %v && %v.create_at <= %v)", tableName, start, tableName, end)

			ctx = context.WithValue(ctx,
				"condition_customized", s,
			)
			delete(reqArgs, "end")
		}
		delete(reqArgs, "start")
	}

	for k, v := range reqArgs {

		var (
			equalCondition = true
			op             = "="
			kk             = fmt.Sprintf("%v.%v", tableName, k)
		)

		if ctxCondition, ok := ctx.Value("condition").(map[string]string); ok {
			if vv, ok := ctxCondition[k]; ok {
				equalCondition = false
				op = vv
			}
		}
		if strings.Index(k, ".") > 0 {
			kk = k
		}

		cond := fmt.Sprintf("%v %v %v", kk, op, v)

		if equalCondition && reflect.TypeOf(v).Kind() == reflect.String {
			cond = fmt.Sprintf("%v = '%v'", kk, v)
		}

		conditions = append(conditions, cond)
	}

	if u, ok := ctx.Value("join").(string); ok {
		tableName = fmt.Sprintf("%v %v ", tableName, u)
	}

	if s, ok := ctx.Value("condition_customized").(string); ok {
		conditions = append(conditions, s)
	}

	if s, ok := ctx.Value("group_by").(string); ok {
		groupBy = fmt.Sprintf("GROUP BY %v", s)
	}

	if s, ok := ctx.Value("order_by").(string); ok {
		orderBy = fmt.Sprintf("ORDER BY %v", s)
	}

	if !p.OnlyData || (p.OnlyDataButTotal && p.OnlyData) {
		var (
			err           error
			total         uint64
			queryCountSQL = fmt.Sprintf("SELECT COUNT(*) FROM %v WHERE %v", tableName, strings.Join(conditions, " AND "))
		)

		log.Debug("sql:%v", queryCountSQL)
		//err := db.QueryRow(queryCountSQL).Scan(&total)

		if tx, ok := ctx.Value("transaction").(*sql.Tx); ok {
			err = tx.QueryRow(queryCountSQL).Scan(&total)
		} else {
			err = db.QueryRow(queryCountSQL).Scan(&total)
		}

		if err != nil {
			return nil, err
		}
		ret["total"] = total
	}

	var (
		querySQL = fmt.Sprintf("SELECT %v FROM %v WHERE %v %v %v ", strings.Join(repKeys, ","), tableName, strings.Join(conditions, " AND "), groupBy, orderBy)
	)
	if p != emptyPagination && !p.OnlyData {
		querySQL += fmt.Sprintf(" LIMIT %v OFFSET %v", p.PageSize, (p.PageIndex-1)*p.PageSize)
	}

	var (
		rows *sql.Rows
		err  error
	)
	if tx, ok := ctx.Value("transaction").(*sql.Tx); ok {
		rows, err = tx.Query(querySQL)
	} else {
		rows, err = db.Query(querySQL)
	}

	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var (
		ty   interface{} = e
		list []interface{}
	)

	if u, ok := ctx.Value("column_types").(interface{}); ok {
		//tableName = fmt.Sprintf("%v %v ", tableName, u)
		ty = u
	}

	for _, v := range repKeys {
		if v == "*" {
			allKeys = true
			break
		}

		ss := strings.Split(v, " ")
		vv := strings.Trim(ss[len(ss)-1], "`")
		repKeyMap[vv] = struct{}{}
	}

	for rows.Next() {
		var (
			dest      []interface{}
			tyOf      = reflect.TypeOf(ty)
			numFields = tyOf.NumField()
			item      = reflect.New(tyOf)
			v2tIndex  = map[int]int{}
			destIndex int
		)

		for i := 0; i < numFields; i++ {

			filedName := tyOf.Field(i).Tag.Get("json")

			if _, ok := repKeyMap[filedName]; ok || allKeys {
				dest = append(dest, reflect.New(tyOf.Field(i).Type).Interface())
				v2tIndex[destIndex] = i
				destIndex++
			}
		}

		if err = rows.Scan(
			dest...,
		); err != nil {
			log.Err(fmt.Sprintf("scan err,%+v", err))
			continue
		}

		for i, v := range dest {

			var (
				ii = v2tIndex[i]
				f  = item.Elem().Field(ii)
			)

			if f.CanSet() {
				item.Elem().Field(ii).Set(reflect.ValueOf(v).Elem())
			} else {
				log.Err(fmt.Sprintf("scan err,%+v", err))
			}
		}

		list = append(list, item.Interface())
	}

	ret["list"] = list

	return ret, nil
}

func (d *DB) QueryRaw(ctx context.Context, querySQL string, out interface{}) ([]interface{}, error) {

	log.Info("sql:%v", querySQL)

	var (
		err  error
		rows *sql.Rows
	)

	if tx, ok := ctx.Value("transaction").(*sql.Tx); ok {
		rows, err = tx.QueryContext(context.Background(), querySQL)
	} else {
		rows, err = db.QueryContext(context.Background(), querySQL)
	}

	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var list []interface{}
	for rows.Next() {
		var (
			dest      []interface{}
			tyOf      = reflect.TypeOf(out)
			numFields = tyOf.NumField()
			item      = reflect.New(tyOf)
			v2tIndex  = map[int]int{}
			destIndex int
		)

		for i := 0; i < numFields; i++ {

			if v, ok := tyOf.Field(i).Tag.Lookup("db"); !ok {
				continue
			} else {
				if v == "-" {
					continue
				}
			}

			dest = append(dest, reflect.New(tyOf.Field(i).Type).Interface())
			v2tIndex[destIndex] = i
			destIndex++
		}

		if err := rows.Scan(
			dest...,
		); err != nil {
			log.Err(fmt.Sprintf("scan err,%+v", err))
			continue
		}

		for i, v := range dest {

			var (
				ii = v2tIndex[i]
				f  = item.Elem().Field(ii)
			)

			if f.CanSet() {
				item.Elem().Field(ii).Set(reflect.ValueOf(v).Elem())
			} else {
				log.Err(fmt.Sprintf("scan err,%+v", err))
			}
		}

		list = append(list, item.Interface())
	}

	return list, nil
}

func (d *DB) TransactionTx(ctx context.Context, fn func(ctx context.Context, t *mysql.Tx) error, args ...interface{}) error {

	var opts *sql.TxOptions
	if len(args) == 1 {
		opts = args[0].(*sql.TxOptions)
	}

	tx, err := d.DB.BeginTx(context.Background(), opts)
	if err != nil {
		return err
	}

	defer func() {
		if r := recover(); r != nil {
			_ = tx.Rollback()
			log.Err(fmt.Sprintf("transaction aborted due to %v", r))
		}
	}()

	ctx = context.WithValue(ctx, "transaction", tx)
	if err := fn(ctx, &mysql.Tx{Tx: tx}); err != nil {
		_ = tx.Rollback()
		return err
	}

	return tx.Commit()
}
